[CloudFront+S3]HTTPレスポンスヘッダのContent-Typeにcharset=UTF-8を指定する
吉川@広島です。
CloudFront+S3なSPAにLambda@Edge(もしくはCloudFront Functions)でセキュリティに関するレスポンスヘッダを追加する、というのはよくやると思います。
その中で、今回は、
安全なウェブサイトの作り方 - 1.5 クロスサイト・スクリプティング:IPA 独立行政法人 情報処理推進機構
で紹介されている、
HTTPレスポンスヘッダのContent-Typeフィールドに文字コード(charset)を指定する。
に対応してみました。
具体的な危険性は、
HTTPのレスポンスヘッダのContent-Typeフィールドには、「Content-Type: text/html; charset=UTF-8」のように、文字コード(charset)を指定できます。この指定を省略した場合、ブラウザは、文字コードを独自の方法で推定して、推定した文字コードにしたがって画面表示を処理します。たとえば、一部のブラウザにおいては、HTMLテキストの冒頭部分等に特定の文字列が含まれていると、必ず特定の文字コードとして処理されるという挙動が知られています。
ということのようです。そのため、
したがって、この問題の解決策としては、Content-Typeの出力時にcharsetを省略することなく、必ず指定することが有効です。
こちらが解消策になるようなので、
- HTMLを返す際、Content-Typeヘッダにcharset=UTF-8を付与する
をゴールとしてLambda@Edgeで対応する方法を考えます。下記を参考に、Content-Typeをカスタマイズしてみました。
Lambda@Edgeを使ってX-Frame-Optionsヘッダを追加してみた | DevelopersIO
サンプルプロジェクト作成
S3+CloudFront上にホスティングするSPAの想定なので、viteのreact-tsテンプレートからサンプルプロジェクトを生成し、S3+CloudFrontにデプロイしました。
npm init vite@latest sample-project -- --template react-ts
方法1 Lambda@Edge
まずLambda@Edgeを使う場合です。以下の内容でorigin reponseとしてLambda@Edgeをデプロイします。
'use strict' exports.handler = (event, context, callback) => { const response = event.Records[0].cf.response const headers = response.headers if (headers['content-type']?.[0]?.value === 'text/html') { headers['content-type'] = [ { key: 'Content-Type', value: 'text/html; charset=UTF-8' }, ] } callback(null, response) }
意図通り動作してくれました。
注意(失敗コード)
ちなみに、上の例のif文をなくして無条件に text/html; charset=UTF-8
を代入した場合も試してみます。
'use strict' exports.handler = (event, context, callback) => { const response = event.Records[0].cf.response const headers = response.headers // if (headers['content-type'][0].value === 'text/html') がない場合 headers['content-type'] = [ { key: 'Content-Type', value: 'text/html; charset=UTF-8' }, ] callback(null, response) }
この内容でLambda@Edgeにデプロイしました。すると、Chromeで表示した際に次のようなエラーとなり、JSが実行できず画面全体は真っ白となってしまいました。
vendor.412157d2.js:1 Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec. index.85056a6b.js:1 Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec.
DeveloperToolのNetworkを見ると原因がわかりました。
ページ自体はtext/htmlで返っています。これは意図通りです。
問題はこちら。JSファイルもtext/htmlで返してしまっています。これは当然おかしいので、
- text/htmlの場合のみ値を
text/html; charset=UTF-8
とする
という条件が必要になるというわけでした。
方法2 CloudFront Functions
書き方が若干異なりますが、CloudFront Functionsも同じように実現できます。viewer responseとしてデプロイします。
function handler(event) { var response = event.response var headers = response.headers if (headers['content-type'].value === 'text/html') { headers['content-type'] = { value: 'text/html; charset=UTF-8' } } return response }
細かい点ですが、CloudFrontFunctionsは原則ES5なので、Lambda@Edgeの時とは異なりoptional chainingは使っていません。
方法3 S3へのアップロード時にメタデータを指定する
Lambda@EdgeやCloudFrontFunctionsを使わず、そもそもS3にアップロードするタイミングでContent-Typeを指定しておく方法もあります。
例えば、デプロイにs3 syncコマンドを使っているのであれば、次のような手順となります。
# まずHTML以外をアップロードする aws s3 sync /path/to/dist s3://{BUCKET_NAME} \ --exclude *.html # Content-Typeを'text/html; charset=UTF-8'と指定してHTMLのみアップロードする aws s3 sync /path/to/dist s3://{BUCKET_NAME} \ --exclude '*' \ --include *.html \ --content-type 'text/html; charset=UTF-8'
これでS3に保存されているファイル自体のメタデータについて Content-Type: text/html; charset=UTF-8
とすることができました。ブラウザからレスポンスヘッダを確認した場合も、しっかりこの値は反映されていました。
参考
- S3 に格納したリソースを CloudFront で配信する構成をCDKで作る | DevelopersIO
- 安全なウェブサイトの作り方 - 1.5 クロスサイト・スクリプティング:IPA 独立行政法人 情報処理推進機構
- @aws-cdk/aws-s3-deployment module · AWS CDK
- Lambda@Edgeを使ってX-Frame-Optionsヘッダを追加してみた | DevelopersIO
- amazon-cloudfront-functions/add-security-headers at main · aws-samples/amazon-cloudfront-functions
- AWS CLI で S3 オブジェクトのメタデータを変更する - michimani.net
- amazon web services - How do I set Content-Type when uploading to S3 with AWS CLI? - Stack Overflow
- AWS CLI での高レベル (S3) コマンドの使用 - AWS Command Line Interface